# ContentKNNAlgorithm.py
from surprise import AlgoBase
from surprise import PredictionImpossible
from AnimeListLoader import AnimeListLoader
import math
import numpy as np
import heapq

# todo
class ContentKNNAlgorithm(AlgoBase):
    def __init__(self, k: int = 40, sim_options: dict = {}):
        """
        初始化 ContentKNNAlgorithm 类。
        
        参数:
            k (int): 最近邻的数量，默认为 40。
            sim_options (dict): 相似度计算选项，默认为空字典。
        """
        AlgoBase.__init__(self)
        self.k = k

    def fit(self, trainset) -> 'ContentKNNAlgorithm':
        """
        训练模型并计算基于内容的相似度矩阵。
        
        参数:
            trainset: 训练数据集。
        
        返回:
            self: 当前实例。
        """
        AlgoBase.fit(self, trainset)

        # 加载每部电影的类型向量
        ml = AnimeListLoader()
        genres = ml.getGenres()
        years = ml.getYears()
        mes = ml.getMiseEnScene()

        print("Computing content-based similarity matrix...")

        # 计算每对电影之间的相似度矩阵
        self.similarities = np.zeros((self.trainset.n_items, self.trainset.n_items))

        for thisRating in range(self.trainset.n_items):
            if thisRating % 100 == 0:
                print(thisRating, " of ", self.trainset.n_items)
            for otherRating in range(thisRating + 1, self.trainset.n_items):
                thisMovieID = int(self.trainset.to_raw_iid(thisRating))
                otherMovieID = int(self.trainset.to_raw_iid(otherRating))
                genreSimilarity = self.computeGenreSimilarity(thisMovieID, otherMovieID, genres)
                yearSimilarity = self.computeYearSimilarity(thisMovieID, otherMovieID, years)
                mesSimilarity = self.computeMiseEnSceneSimilarity(thisMovieID, otherMovieID, mes)
                self.similarities[thisRating, otherRating] = genreSimilarity * yearSimilarity * mesSimilarity
                self.similarities[otherRating, thisRating] = self.similarities[thisRating, otherRating]

        print("...done.")
        return self

    def computeGenreSimilarity(self, movie1: int, movie2: int, genres: dict) -> float:
        """
        计算两部电影的类型相似度。
        类型一般是离散的位向量([1,0,0,1]), 适合使用余弦相似度
        参数:
            moviel (int): 第一部电影的 ID。
            movie2 (int): 第二部电影的 ID。
            genres (dict): 每部电影的类型向量。
        
        返回:
            float: 类型相似度值。
        """
        genres1 = genres[movie1]
        genres2 = genres[movie2]
        sumxx, sumxy, sumyy = 0, 0, 0
        for i in range(len(genres1)):
            x = genres1[i]
            y = genres2[i]
            # 计算x的模
            sumxx += x * x
            # 计算y的模
            sumyy += y * y
            # 计算xy点积
            sumxy += x * y

        return sumxy / math.sqrt(sumxx * sumyy)

    def computeYearSimilarity(self, movie1: int, movie2: int, years: dict) -> float:
        """
        计算两部电影的上映年份相似度。
        
        参数:
            movie1 (int): 第一部电影的 ID。
            movie2 (int): 第二部电影的 ID。
            years (dict): 每部电影的上映年份。
        
        返回:
            float: 年份相似度值。
        """
        diff = abs(years[movie1] - years[movie2])
        sim = math.exp(-diff / 10.0)
        return sim

    def computeMiseEnSceneSimilarity(self, movie1: int, movie2: int, mes: dict) -> float:
        """
        计算两部电影的视觉场景相似度。
        
        参数:
            movie1 (int): 第一部电影的 ID。
            movie2 (int): 第二部电影的 ID。
            mes (dict): 每部电影的视觉场景特征。
        
        返回:
            float: 视觉场景相似度值。
        """
        mes1 = mes[movie1]
        mes2 = mes[movie2]
        if mes1 and mes2:
            shotLengthDiff = math.fabs(mes1[0] - mes2[0])
            colorVarianceDiff = math.fabs(mes1[1] - mes2[1])
            motionDiff = math.fabs(mes1[3] - mes2[3])
            lightingDiff = math.fabs(mes1[5] - mes2[5])
            numShotsDiff = math.fabs(mes1[6] - mes2[6])
            return shotLengthDiff * colorVarianceDiff * motionDiff * lightingDiff * numShotsDiff
        else:
            return 0

    def estimate(self, u: int, i: int) -> float:
        """
        预测评分。
        
        参数:
            u (int): 用户 ID。
            i (int): 项目（电影）ID。
        
        返回:
            float: 预测评分。
        
        异常:
            PredictionImpossible: 如果用户或项目未知。
        """
        try:
            if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)):
                raise PredictionImpossible('User and/or item is unknown.')

            neighbors = []
            for rating in self.trainset.ur[u]: # [itemID, rating]
                # similarities: [(itemID1, itemID2)...]
                genreSimilarity = self.similarities[i, rating[0]]
                neighbors.append((genreSimilarity, rating[1]))

            k_neighbors = heapq.nlargest(self.k, neighbors, key=lambda t: t[0])

            simTotal = weightedSum = 0
            for simScore, rating in k_neighbors:
                if simScore > 0:
                    simTotal += simScore
                    weightedSum += simScore * rating

            if simTotal == 0:
                raise PredictionImpossible('No neighbors')

            predictedRating = weightedSum / simTotal
            return predictedRating
        except PredictionImpossible:
            # 提供默认推荐逻辑
            return self.default_recommendation(u, i)

    def default_recommendation(self, u: int, i: int) -> float:
        # 默认推荐逻辑，例如返回平均评分
        return 3.0  # 假设平均评分为3.0
